package com.bolt.admin.module.sys.service;


import com.bolt.admin.config.RsaProperties;
import com.bolt.admin.constant.CacheKey;
import com.bolt.admin.exception.BizExceptionEnum;
import com.bolt.admin.module.sys.dto.LoginReqDTO;
import com.bolt.admin.module.sys.dto.UserDTO;
import com.bolt.admin.module.sys.dto.UserQueryCriteria;
import com.bolt.admin.module.sys.entity.*;
import com.bolt.admin.module.sys.repository.OrgRepository;
import com.bolt.admin.module.sys.repository.UserLogonLogRepository;
import com.bolt.admin.module.sys.repository.UserRepository;
import com.bolt.admin.module.sys.service.converter.UserConverter;
import com.bolt.admin.properties.LoginProperties;
import com.bolt.admin.security.shiro.AuthContextHolder;
import com.bolt.admin.security.shiro.DefaultAuthUserDetails;
import com.bolt.admin.security.shiro.PasswordUtil;
import com.bolt.admin.security.shiro.jwt.OnlineUser;
import com.bolt.admin.security.shiro.jwt.TokenProvider;
import com.bolt.admin.security.shiro.jwt.UserInfo;
import com.bolt.common.BoltConstants;
import com.bolt.common.collection.IteratorUtil;
import com.bolt.common.convert.Convert;
import com.bolt.common.io.FileUtil;
import com.bolt.common.lang.Assert;
import com.bolt.common.reflect.ObjectUtil;
import com.bolt.common.security.RSAUtil;
import com.bolt.common.utils.StrUtil;
import com.bolt.common.utils.UidUtil;
import com.bolt.common.web.RequestUtil;
import com.bolt.convention.data.PageInfo;
import com.bolt.convention.data.code.CommonCode;
import com.bolt.convention.exception.ServiceException;
import com.bolt.support.base.service.BaseService;
import com.bolt.support.cache.redis.RedisManager;
import com.bolt.support.captcha.CaptchaService;
import com.bolt.support.captcha.CaptchaVO;
import com.bolt.support.spring.jpa.filter.FilterBuilder;
import com.bolt.support.spring.jpa.jpql.Clauses;
import com.bolt.support.spring.jpa.jpql.Ops;
import com.bolt.support.spring.jpa.repository.BasicJpaRepository;
import com.bolt.support.spring.mvc.bean.ResourceProperties;
import eu.bitwalker.useragentutils.UserAgent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static com.bolt.common.utils.PasswordUtil.entryptPassword;


/**
 * Auto Generate Javadoc
 *
 * @author 日期:2018-02-26 16:46:13
 *         用户信息表
 **/
@Service
@Transactional(readOnly = true, rollbackFor = Exception.class)
public class UserService extends BaseService<UserEntity, String> {


    @Autowired
    private UserRepository userRepository;

    @Autowired
    private OrgRepository orgRepository;

    @Autowired
    private UserLogonLogRepository userLogonLogRepository;

    @Autowired
    private PrivilegeService privilegeService;

    @Autowired
    private RedisManager redisManager;

    @Autowired
    private LoginProperties loginProperties;

    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private OnlineUserService onlineUserService;

    @Autowired
    private ResourceProperties resourceProperties;

    @Autowired
    private VerifyService verifyService;

    @Autowired
    private CaptchaService captchaService;

    private static String LOGON_USER_LOCK = "user_lock_key";

    private static String AVATAR_DIR = "avatar";

    @Override
    protected BasicJpaRepository<UserEntity, String> getRepository() {
        return userRepository;
    }


    public PageInfo<UserDTO> findPage(Pageable pageable, UserQueryCriteria userDTO) {
        return jpaQueryFactory.select(UserDTO.class,
                Clauses.of(UserEntity::getId),

                Clauses.of(UserEntity::getUserName),
                Clauses.of(UserEntity::getRealName),
                Clauses.of(UserEntity::getMobile),
                Clauses.of(UserEntity::getEmail),
                Clauses.of(UserEntity::getNickName),
                Clauses.of(UserEntity::getOrgId),
                Clauses.of(UserEntity::getEnabled),
                Clauses.of(UserEntity::getCreateDate),
                Clauses.of(OrgEntity::getName).as("orgName"),
                Clauses.of(UserR2Role::getRoleId).group_concat().as("roleIds")
        ).from(Clauses.of(UserEntity.class))
                .leftJoin(Clauses.of(OrgEntity.class))
                .on(Clauses.of(UserEntity::getOrgId), Clauses.of(OrgEntity::getId))
                .leftJoin(Clauses.of(UserR2Role.class))
                .on(Clauses.of(UserR2Role::getUserId), Clauses.of(UserEntity::getId))
                .and(Ops.EQ, Clauses.of(OrgEntity::getId).as("orgId"))

                .and(Ops.EQ, Clauses.of(UserEntity::getEnabled))
                .and(Ops.BETWEEN, Clauses.of(UserEntity::getCreateDate).as("createTime"))
                .or(Ops.LIKELEFT, Clauses.of(UserEntity::getUserName).as("blurry"),
                        Clauses.of(UserEntity::getMobile).as("blurry"),
                        Clauses.of(UserEntity::getEmail).as("blurry"))
                .buildCondition(userDTO)
                .groupBy(Clauses.of(UserEntity::getId),
                        Clauses.of(UserEntity::getId),
                        Clauses.of(UserEntity::getUserName),
                        Clauses.of(UserEntity::getRealName),
                        Clauses.of(UserEntity::getMobile),
                        Clauses.of(UserEntity::getEmail),
                        Clauses.of(UserEntity::getNickName),
                        Clauses.of(UserEntity::getOrgId),
                        Clauses.of(UserEntity::getEnabled),
                        Clauses.of(UserEntity::getCreateDate),
                        Clauses.of(OrgEntity::getName))

                .fetchPage(pageable);
    }


    /**
     * @param userId
     * @return
     */
    public UserEntity findByUserId(String userId) {
        return redisManager.getOrCache(CacheKey.USER_ID + userId, UserEntity.class, () -> {
            Optional<UserEntity> userEntity = this.findOne(userId);
            return userEntity.orElseGet(UserEntity::new);
        }, RedisManager.ONE_HOUR * 2);
    }

    /**
     * 按用户ID查询用户信息
     */
    public Optional<UserEntity> findByUserName(String userName) {
        return this.findOne((FilterBuilder<UserEntity> f) ->
                f.lambda().eq(UserEntity::getUserName, userName).build()
        );
    }

    /**
     * 按用户用手号查询用户信息
     *
     * @param mobile
     * @return
     */
    public Optional<UserEntity> findByMobile(String mobile) {
        return this.findOne((FilterBuilder<UserEntity> f) ->
                f.lambda().eq(UserEntity::getMobile, mobile).build()
        );
    }

    public UserInfo userInfo(String userId) {
        UserInfo userInfo = redisManager.getOrCache(CacheKey.USER_ID + userId, UserInfo.class, () -> {
            UserEntity userEntity = findOne(userId).orElseThrow(() -> new ServiceException(BizExceptionEnum.DATA_NOT_FIND));
            UserInfo info = new UserInfo();
            info.setUserName(userEntity.getUserName());
            info.setRealName(userEntity.getRealName());
            info.setUserId(userEntity.getId());
            info.setMobile(userEntity.getMobile());
            info.setEmail(userEntity.getEmail());
            info.setAvatarPath(userEntity.getAvatarPath());
            info.setIsAdmin(userEntity.getAdmin());
            //info.setPassword(account.getPassword());
            // info.setAuthSalt(account.getAuthSalt());
            OrgEntity org = userEntity.getOrgInfo();
            if (ObjectUtil.isNotNull(org)) {
                info.setAreaCode(org.getAreaCode());
                info.setOrgCode(org.getCode());
                info.setOrgName(org.getName());
                info.setOrgId(org.getId());
            }
            Set<String> roles = new LinkedHashSet<>();
            Set<String> stringPermissions = new LinkedHashSet<>();
            if (null != userEntity) {
                //查询用户角色列表
                List<RoleEntity> roleEntities = userEntity.getRoles();
                for (RoleEntity roleDO : roleEntities) {
                    roles.add(roleDO.getCode());
                }
                //超级管理员特殊处理
                if (userEntity.getAdmin()) {
                    //追加超级权限配置
                    stringPermissions.add("*");
                }
                //基于当前用户所有角色集合获取有效的权限集合
                if (IteratorUtil.isEmpty(stringPermissions)) {

                    List<PrivilegeEntity> privileges = privilegeService.findPrivileges(roleEntities);
                    for (PrivilegeEntity privilegeEntity : privileges) {
                        stringPermissions.add(privilegeEntity.getCode());
                    }
                }

            }

            info.setPermissions(stringPermissions);
            info.setRoles(roles);
            return info;
        }, RedisManager.ONE_HOUR * 2);
        return userInfo;
    }

    /**
     * 保存后台操作用户
     *
     * @param userDTO
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public void saveOrUpdate(UserDTO userDTO) {

        OrgEntity orgEntity = orgRepository.findById(userDTO.getOrgId())
                .orElseThrow(() -> new ServiceException(BizExceptionEnum.DATA_NOT_FIND));

        UserEntity user = UserConverter.toEntity(userDTO);
        if (user.isNew()) {
            user.setOrgInfo(orgEntity);
            user.setAdmin(false);
            long count = userRepository.findCountByUserName(user.getUserName());
            Assert.isTrue(count > 0, "用户名已经存在,请重新输入!");
            user.setAuthSalt(UidUtil.UID());
            // 首次创建密码为登录ID(统一分配置)
            String password = encodeUserPasswd(user, BoltConstants.DEAULT_PASSWORD);
            user.setPassword(password);
            if (StrUtil.isBlank(user.getRealName())) {
                user.setRealName(user.getUserName());
            }
            setRoleIds(userDTO.getRoleIds(), user);
            save(user);
        } else {
            UserEntity entity = userRepository.findById(userDTO.getId())
                    .orElseThrow(() -> new ServiceException(BizExceptionEnum.DATA_NOT_FIND));
            UserConverter.toEntity(entity, userDTO);
            entity.setOrgInfo(orgEntity);
            setRoleIds(userDTO.getRoleIds(), entity);
            save(entity);
            redisManager.del(CacheKey.USER_ID + entity.getId());
        }
    }

    /**
     * 处理密码加密
     *
     * @param user
     * @param rawPassword
     * @return
     */
    public String encodeUserPasswd(UserEntity user, String rawPassword) {
        return entryptPassword(rawPassword, user.getAuthSalt());
    }

    private void setRoleIds(String roleIds, UserEntity entity) {
        List<UserR2Role> userR2Roles = entity.getUserR2Roles();
        //没有关联对象集合，清空关联集合
        if (StrUtil.isEmpty(roleIds) && IteratorUtil.isNotEmpty(userR2Roles)) {
            userR2Roles.clear();
        } else {
            List<String> ids = StrUtil.split(roleIds, ',', true, true);
            //有关联对象集合，如果原来没有则初始化创建，否则移除不存在关联
            if (IteratorUtil.isNotEmpty(userR2Roles)) {
                userR2Roles.removeIf(r2 -> !ids.stream().anyMatch(roleId -> roleId.equals(r2.getRole().getId())));
                ids.removeIf(roleId -> entity.getUserR2Roles().stream().anyMatch(r2 -> r2.getRole().getId().equals(roleId)));
            }
            List<UserR2Role> newRole = new ArrayList<>();
            ids.forEach(roleId -> {
                UserR2Role r2 = new UserR2Role();
                r2.setUser(entity);
                RoleEntity role = new RoleEntity();
                role.setId(roleId);
                r2.setRole(role);
                newRole.add(r2);
            });
            if (ObjectUtil.isNotNull(userR2Roles)) {
                userR2Roles.addAll(newRole);
            } else {
                entity.setUserR2Roles(newRole);
            }
        }
    }

    /**
     * 逻辑删除用户
     *
     * @param id
     */
    @Transactional(rollbackFor = Exception.class)
    public void deleteUser(String id) {
        jpaQueryFactory.delete(UserEntity.class)
                .where(Clauses.of().eq(Clauses.of(UserEntity::getId), id))
                .execute();
        redisManager.del(id);
    }

    /**
     * 处理尝试登录失败用户锁定功能
     *
     * @param userName
     */
    public int setfailureLimit(String userName) {
        int failure = Convert.toInt(redisManager.get(userName), 0);
        if (failure > loginProperties.getLogonFailureLimit()) {
            redisManager.cache(LOGON_USER_LOCK + userName, "true", loginProperties.getLockedTime(), TimeUnit.MINUTES);
        } else {
            failure = failure + 1;
            redisManager.cache(userName, String.valueOf(failure), 1, TimeUnit.MINUTES);
        }
        return failure;
    }

    public boolean isUserLock(String userName) {
        return Convert.toBool(redisManager.get(LOGON_USER_LOCK + userName), false);
    }

    /**
     * 保存登录日志
     *
     * @param logonLog
     */
    @Transactional(rollbackFor = Exception.class)
    public void saveLoginLog(LoginLogEntity logonLog) {
        userLogonLogRepository.save(logonLog);
    }


    public String login(LoginReqDTO loginReqDTO) throws Exception {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes())
                .getRequest();

        String graphId = loginReqDTO.getDeviceId();
        String username = loginReqDTO.getUsername();
        String password = RSAUtil.decryptByPrivateKey(RsaProperties.privateKey, loginReqDTO.getPassword().toString());
        String validateData = loginReqDTO.getValidateData();

        boolean isSuccess = captchaService.verification(new CaptchaVO(graphId, validateData));
        if(!isSuccess){
            throw new ServiceException(BizExceptionEnum.AUTH_CAPTCHA_VERIFY_ERROR);
        }
        if (isUserLock(username)) {
            throw new ServiceException(BizExceptionEnum.ACCOUNT_LOCKED);
        }
        UserEntity account = findByUserName(username).orElseThrow(() ->
                new ServiceException(BizExceptionEnum.USER_NOT_EXISTED));
        String entryptPassword = PasswordUtil.entryptPassword(password, account.getAuthSalt());
        if (!entryptPassword.equals(account.getPassword())) {
            throw new ServiceException(BizExceptionEnum.AUTH_REQUEST_ERROR);
        }
        String accessToken = tokenProvider.createToken(account.getId());
        LocalDateTime now = LocalDateTime.now();
        //写入登入记录信息
        LoginLogEntity userLoginLog = new LoginLogEntity();
        userLoginLog.setUser(account);
        userLoginLog.setLogonTime(now);
        userLoginLog.setRemoteAddr(request.getRemoteAddr());
        userLoginLog.setRemoteHost(request.getRemoteHost());
        userLoginLog.setRemotePort(request.getRemotePort());
        userLoginLog.setUserAgent(request.getHeader("User-Agent"));
        userLoginLog.setXforwardFor(RequestUtil.getRemoteIpAddress(request));
        saveLoginLog(userLoginLog);
        OnlineUser onlineUser = new OnlineUser();
        UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
        // 获取客户端操作系统
        String os = userAgent.getOperatingSystem().getName();
        // 获取客户端浏览器
        String browser = userAgent.getBrowser().getName();
        onlineUser.setIp(RequestUtil.getRemoteIpAddress(request));
        onlineUser.setBrowser(browser);
        onlineUser.setOs(os);
        onlineUser.setLoginTime(new Date());
        onlineUser.setUserId(account.getId());
        onlineUser.setUserName(account.getUserName());
        if (ObjectUtil.isNotNull(account.getOrgInfo())) {
            onlineUser.setOrgName(account.getOrgInfo().getName());
        }
        onlineUser.setNickName(account.getRealName());
        onlineUser.setKey(accessToken);
        onlineUserService.save(onlineUser, account.getId(), loginProperties.getJwt().getExpiration());
        return accessToken;
    }

    /**
     * 当前用户修改密码
     *
     * @param oldPassword
     * @param newPassword
     */
    @Transactional(rollbackFor = Exception.class)
    public void updatePassword(String oldPassword, String newPassword) {

        UserEntity userEntity = findOne(AuthContextHolder.getAuthUserDetails().getUserId())
                .orElseThrow(() -> new ServiceException(CommonCode.DATA_NOT_FOUND));
        oldPassword = encodeUserPasswd(userEntity, oldPassword);
        if (!oldPassword.equals(userEntity.getPassword())) {
            throw new ServiceException("原始密码不正确");
        }
        newPassword = encodeUserPasswd(userEntity, newPassword);
        if (oldPassword.equals(newPassword)) {
            throw new ServiceException("变更密码不能与当前密码一样");
        }
        userEntity.setPassword(newPassword);
        userEntity.setPwdResetTime(new Date());
        save(userEntity);
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateEmail(String code, String password, String email) {
        UserEntity userEntity = findOne(AuthContextHolder.getAuthUserDetails().getUserId())
                .orElseThrow(() -> new ServiceException(CommonCode.DATA_NOT_FOUND));
        password = encodeUserPasswd(userEntity, password);
        if (password.equals(userEntity.getPassword())) {
            throw new ServiceException("密码不正确");
        }
        verifyService.validated(BoltConstants.EMAIL_RESET_EMAIL_CODE_KEY + email, code);
        userEntity.setEmail(email);
        save(userEntity);
        redisManager.del(userEntity.getId());
    }

    /**
     * 用于超级管理员处理统一的密码重置
     *
     * @param userId
     */
    public void resetPassword(String userId) {
        DefaultAuthUserDetails defaultAuthUserDetails = (DefaultAuthUserDetails) AuthContextHolder.getAuthUserDetails();
        UserEntity userEntity = findOne(userId)
                .orElseThrow(() -> new ServiceException(CommonCode.DATA_NOT_FOUND));

        if (defaultAuthUserDetails.getUserInfo().getIsAdmin()) {
            String password = encodeUserPasswd(userEntity, BoltConstants.DEAULT_PASSWORD);
            userEntity.setPassword(password);
            userEntity.setPwdResetTime(new Date());
            save(userEntity);
            redisManager.del(CacheKey.USER_ID + userEntity.getId());
        } else {
            throw new ServiceException(CommonCode.INVALID_AUTH);
        }
    }

    /**
     * 用户中心修改个人信息
     *
     * @param userDTO
     */
    public void updateCenter(UserDTO userDTO) {
        UserEntity userEntity = findOne(userDTO.getId())
                .orElseThrow(() -> new ServiceException(CommonCode.DATA_NOT_FOUND));
        UserEntity user1 = findByMobile(userDTO.getMobile()).orElseGet(UserEntity::new);
        if (user1 != null && !userEntity.getId().equals(user1.getId())) {
            throw new ServiceException("-1", "手机号已经存在，不能重复");
        }
        this.jpaQueryFactory.update(UserEntity.class)
                .set(Clauses.of(UserEntity::getRealName), userDTO.getRealName())
                .set(Clauses.of(UserEntity::getMobile), userDTO.getMobile())
                .where(Clauses.of().eq(Clauses.of(UserEntity::getId), userEntity.getId()))
                .execute();
        redisManager.del(CacheKey.USER_ID + userEntity.getId());
    }

    /**
     * 上传个人头像
     *
     * @param multipartFile
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public Map<String, String> updateAvatar(MultipartFile multipartFile) {
        DefaultAuthUserDetails defaultAuthUserDetails = (DefaultAuthUserDetails) AuthContextHolder.getAuthUserDetails();
        UserEntity userEntity = findOne(defaultAuthUserDetails.getUserId())
                .orElseThrow(() -> new ServiceException(CommonCode.DATA_NOT_FOUND));
        if (null != multipartFile && !multipartFile.isEmpty()) {
            try {
                String name = UidUtil.UID();
                String suffix = FileUtil.extName(multipartFile.getOriginalFilename());
                String fileName = name + "." + suffix;
                Path filePath = Paths.get(resourceProperties.getUploadPath(), AVATAR_DIR, fileName);
                FileUtil.writeBytes(multipartFile.getBytes(), filePath.toString());
                userEntity.setAvatarPath(fileName);
                save(userEntity);
                redisManager.del(CacheKey.USER_ID + userEntity.getId());
                return new HashMap<String, String>(1) {{
                    put("avatar", fileName);
                }};
            } catch (Exception ex) {
                throw new ServiceException(ex);
            }
        }
        return null;
    }
}
